﻿#region Copyright and License Notice
// Copyright (C)2010-2016 - Rob Levine and other contributors
// http://configgen.codeplex.com
// 
// This file is part of ConfigGen.
// 
// ConfigGen is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// ConfigGen is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License and 
// the GNU Lesser General Public License along with ConfigGen.  
// If not, see <http://www.gnu.org/licenses/>
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using ConfigGen.Utilities.Extensions.System;

namespace ConfigGen.Core.TokenHandling
{
    /// <summary>
    /// This class locates tokens (delimited by the specified start/end delimiters supplied at construction time)
    /// and calls the specified callback when a token is located.
    /// </summary>
    public class TokenLocator : ITokenLocator
    {
        #region Private Members
        private bool _tokenStartEvent;
        private bool _tokenEndEvent;
        #endregion

        #region Events
        /// <summary>
        /// Raised when a known token is encountered during processing.
        /// </summary>
        public event EventHandler<TokenEventArgs> TokenUsed;
        private void OnTokenUsed(object sender, TokenEventArgs args)
        {
            var handler = TokenUsed;
            if (handler != null)
            {
                handler(sender, args);
            }
        }

        /// <summary>
        /// Raised when an unrecognised token is encountered during processing.
        /// </summary>
        public event EventHandler<TokenEventArgs> UnrecognisedToken;
        private void OnUnrecognisedToken(object sender, TokenEventArgs args)
        {
            var handler = UnrecognisedToken;
            if (handler != null)
            {
                handler(sender, args);
            }
        }

        #endregion

        void TokenEndlocatorDelimiterLocated(object sender, DelimiterLocatedEventArgs e)
        {
            _tokenEndEvent = true;
        }

        void TokenStartLocatorDelimiterLocated(object sender, DelimiterLocatedEventArgs e)
        {
            _tokenStartEvent = true;
        }

        /// <summary>
        /// Locates tokens in the supplied input, calling the supplied callback each time a token is located. The callback return specifies 
        /// the replacement string for the token. The input, with the tokens having been replaced, is written to the supplied output.
        /// </summary>
        /// <param name="input">
        /// Input stream
        /// </param>
        /// <param name="output">
        /// Output stream
        /// </param>
        /// <param name="tokenStartDelimiter">
        /// The token start delimiter.
        /// </param>
        /// <param name="tokenEndDelimiter">
        /// The token end delimiter.
        /// </param>
        /// <param name="tokenLocatedCallback">
        /// Callback to be called when a token is located, whose argument is the token, and whose return value 
        /// is the token replacement string.
        /// </param>
        public void LocateTokens(TextReader input, TextWriter output, string tokenStartDelimiter, string tokenEndDelimiter, Func<string, string> tokenLocatedCallback)
        {
            if (tokenStartDelimiter == null) throw new ArgumentNullException("tokenStartDelimiter");
            if (tokenEndDelimiter == null) throw new ArgumentNullException("tokenEndDelimiter");

            var tokenStartLocator = new DelimiterLocator(tokenStartDelimiter);
            tokenStartLocator.DelimiterLocated += TokenStartLocatorDelimiterLocated;
            var tokenEndlocator = new DelimiterLocator(tokenEndDelimiter);
            tokenEndlocator.DelimiterLocated += TokenEndlocatorDelimiterLocated;

            int iChar;

            var tokenCharQueue = new Queue<char>();
            bool inToken = false;
            bool prevInToken = false;
            while ((iChar = input.Read()) != -1)
            {
                var currentChar = (char)iChar;

                char[] fromStartLocator = tokenStartLocator.Push(currentChar);
                char[] fromEndLocator = tokenEndlocator.Push(currentChar);
                char[] relevantQueue = prevInToken ? fromEndLocator : fromStartLocator;

                prevInToken = inToken;

                if (inToken)
                {
                    if (_tokenEndEvent)
                    {
                        var token = new string(tokenCharQueue.ToArray());
                        tokenCharQueue.Clear();
                        inToken = false;

                        try
                        {
                            string tokenValue = tokenLocatedCallback(token);
                            output.Write(tokenValue);
                            OnTokenUsed(this, new TokenEventArgs(token));
                        }
                        catch (NoMatchingTokenException)
                        {
                            OnUnrecognisedToken(this, new TokenEventArgs(token));
                        }
                        tokenCharQueue.Clear();
                    }
                    else if (_tokenStartEvent)
                    {
                        throw new UnexpectedTokenDelimiterException("Unexpected token start delimiter while already processing a token.");
                    }
                    else
                    {
                        if (!relevantQueue.IsNullOrEmpty())
                        {
                            foreach (var c in relevantQueue)
                            {
                                tokenCharQueue.Enqueue(c);
                            }
                        }
                    }
                }
                else
                {
                    if (_tokenStartEvent)
                    {
                        inToken = true;
                    }
                    else if (_tokenEndEvent)
                    {
                        throw new UnexpectedTokenDelimiterException("Unexpected token end delimiter while not processing a token.");
                    }
                }

                if (!inToken && fromStartLocator != null &&
                    !relevantQueue.IsNullOrEmpty())
                {
                    output.Write(relevantQueue);
                }

                _tokenStartEvent = false;
                _tokenEndEvent = false;
            }

            if (inToken)
            {
                throw new EndOfStreamException("Unexpected end of template stream");
            }

            tokenStartLocator.DelimiterLocated -= TokenStartLocatorDelimiterLocated;
            tokenEndlocator.DelimiterLocated -= TokenEndlocatorDelimiterLocated;
        }
    }
}
